/* global browser */
import Ext, { isFirefox } from '@/common/web_extension';
import { delay, setIn, pick, splitIntoTwo, uid, and } from '@/common/utils';
import * as CONSTANT from '@/common/constant';
import log from '@/common/log';
import clipboard from '@/common/clipboard';
import storage from '@/common/ext_storage_local';
import { setFileInputFiles } from '@/common/debugger';
import { getDownloadMan, DownloadMan } from '@/common/download_man';
import config from '@/config';
import { resizeViewportOfTab, getWindowSize } from '@/common/resize_window';
import { getTab, getCurrentTab, activateTab, updateUrlForTab } from '@/common/tab_utils';
import { setProxy, getProxyManager } from '@/services/proxy';
import { getContextMenuService } from '@/services/contextMenu';
import { getState, updateState } from './common/global_state';
import { getPlayTab, showPanelWindow } from './common/tab';

import { askPanel, askPlayTab, askRecordTab, askTab } from './ipc';

new DownloadMan();
const SIDEPANEL_TAB_ID = 999999999
const SIDEPANEL_PORT_NAME = 'uiv_sidepanel'
const checkTabIsPresent = async (idexId: number, wid: number) => {
    return new Promise((resolve, reject) => {
        Ext.tabs.query({ windowId: wid }, function (tabs: any) {
            let doFlag = '';
            for (let i = tabs.length - 1; i >= 0; i--) {
                if (tabs[i].index === idexId) {
                    doFlag = tabs[i];
                    break;
                }
            }
            resolve(doFlag);
        });
    });
};

const checkWindowisOpen = async (toplayId: number) => {
    return new Promise((resolve, reject) => {
        Ext.tabs.query({}, function (tabs: any) {
            let doFlag = [];
            for (let i = tabs.length - 1; i >= 0; i--) {
                if (tabs[i].id === toplayId) {
                    doFlag = tabs[i];
                    break;
                }
            }
            resolve(doFlag);
        });
    });
};

const getToplayTabId = async () => {
    return new Promise((resolve, reject) => {
        return Ext.tabs.query({ active: true }).then((tabs: any) => {
            // lw:排除popup
            tabs = tabs.filter((tab: any) => tab.url !== Ext.runtime.getURL('panel.html'));
            resolve(tabs[0]);
        });
    });
};
// 显示徽章
const showBadge = (options: any) => {
    const { clear, text, color, blink } = {
        clear: false,
        text: '',
        color: '#ff0000',
        blink: 0,
        ...(options || {}),
    };

    if (clear) {
        return Ext.action.setBadgeText({ text: '' });
    }

    Ext.action.setBadgeBackgroundColor({ color });
    Ext.action.setBadgeText({ text });

    if (blink) {
        setTimeout(() => {
            Ext.action.getBadgeText({}).then((curText: any) => {
                if (curText !== text) return false;
                return Ext.action.setBadgeText({ text: '' });
            });
        }, blink);
    }

    return true;
};
// 切换录制徽章
const toggleRecordingBadge = (isRecording: boolean, options: any) => {
    return showBadge({
        color: '#ff0000',
        text: 'R',
        ...(options || {}),
        clear: !isRecording,
    });
};
// 切换捕获徽章
const toggleInspectingBadge = (isInspecting: boolean, options: any) => {
    return showBadge({
        color: '#ffa800',
        text: 'S',
        ...(options || {}),
        clear: !isInspecting,
    });
};
// 切换播放徽章
const togglePlayingBadge = (isPlaying: boolean, options: any) => {
    return showBadge({
        color: '#14c756',
        text: 'P',
        ...(options || {}),
        clear: !isPlaying,
    });
};

// 通知录制命令成功
const notifyRecordCommand = (command: any) => {
    const notifId = uid();

    Ext.notifications.create(notifId, {
        type: 'basic',
        iconUrl: './logo.png',
        title: '命令录制成功!',
        message: (function () {
            const list = [];

            list.push(`command: ${command.cmd}`);
            if (command.target) { list.push(`target: ${command.target}`); }
            if (command.value) { list.push(`value: ${command.value}`); }

            return list.join('\n');
        })(),
    });

    // Note: close record notifications right away, so that notifications won't be stacked
    setTimeout(() => {
        Ext.notifications.clear(notifId).catch((e: any) => log.error(e));
    }, 2000);
};
// 通知遇到暂停命令自动暂停
const notifyAutoPause = () => {
    Ext.notifications.create({
        type: 'basic',
        iconUrl: './logo.png',
        title: 'Replay paused!',
        message: 'Auto paused by command',
    });
};
// 通知遇到断点自动暂停
const notifyBreakpoint = () => {
    Ext.notifications.create({
        type: 'basic',
        iconUrl: './logo.png',
        title: 'Replay paused!',
        message: 'Auto paused by breakpoint',
    });
};
// 通知打印
const notifyEcho = (text: any) => {
    Ext.notifications.create({
        type: 'basic',
        iconUrl: './logo.png',
        title: 'Echo',
        message: text,
    });
};
// 关闭所有窗口
const closeAllWindows = () => {
    return Ext.windows.getAll().then((wins: any) => {
        return Promise.all(wins.map((win: any) => Ext.windows.remove(win.id)));
    });
};
// 是时候备份了
const isTimeToBackup = () => {
    return storage.get('config').then((config: any) => {
        const { enableAutoBackup, lastBackupActionTime, autoBackupInterval } = config;

        if (!enableAutoBackup) {
            return {
                timeout: false,
                remain: -1,
            };
        }

        const diff = new Date().getTime() - (lastBackupActionTime || 0);
        return {
            timeout: diff > autoBackupInterval * 24 * 3600000,
            remain: diff,
        };
    });
};
// 
const notifyPanelAboutActiveTab = (activeTabId: number) => {
    Promise.all([Ext.tabs.get(activeTabId), Promise.resolve()]).then((tuple) => {
        const [tab] = tuple;
        if (tab.url.indexOf(Ext.runtime.getURL('')) !== -1) return;

        if (!tab.title || tab.title.trim().length === 0) {
            return delay(() => notifyPanelAboutActiveTab(activeTabId), 200);
        }

        return askPanel('UPDATE_ACTIVE_TAB', {
            url: tab.url,
            title: tab.title,
        });
    });
};
// 页签是否active并focused
const isTabActiveAndFocused = (tabId: number) => {
    return Promise.all([Ext.tabs.get(tabId), getState()])
        .then(([tab, state]) => {
            if (!tab.active) return false;

            switch (state.status) {
                case CONSTANT.APP_STATUS.NORMAL:
                    return Ext.windows.get(tab.windowId).then((win: any) => win.focused);

                case CONSTANT.APP_STATUS.PLAYER:
                    return tabId === state.tabIds.toPlay;

                case CONSTANT.APP_STATUS.RECORDER:
                    return tabId === state.tabIds.toRecord;

                default:
                    throw new Error(`E213: isTabActiveAndFocused: unknown app status, '${state.status}'`);
            }
        })
        .catch((e) => false);
};


function logKantuClosing() {
    return Promise.resolve();
}

// 绑定事件
const bindEvents = () => {
    Ext.action.onClicked.addListener(() => {
        // return Ext.sidePanel.open({

        // });
        // Ext.sidePanel
        //     .setOptions({
        //         enabled: true,
        //     })
        return showPanelWindow({})
    });

    Ext.tabs.onRemoved.addListener(async (tabId: number, removeInfo: any) => {
        const state = await getState();

        // Closing playing tab in player mode
        if (state.status === CONSTANT.APP_STATUS.PLAYER && tabId === state.tabIds.toPlay) {
            // Note: If it's closed by `selectWindow tab=close` command, ignore it
            if (state.pendingPlayingTab) {
                return;
            }

            return Ext.windows.get(removeInfo.windowId, { populate: true }).then((win: any) => {
                const pActiveTab = !win
                    ? getCurrentTab().then((tab) => {
                        if (!tab) return null;
                        // Do nothing if window is also closed and Kantu window is focused
                        if (tab.id === state.tabIds.panel) return null;
                        return tab;
                    })
                    : Promise.resolve(win.tabs.find((tab: any) => tab.active));

                return pActiveTab.then((tab) => {
                    if (tab && tab.id) {
                        // This is the main purpose for this callback: Update tabIds.toPlay to new active tab
                        updateState(setIn(['tabIds', 'toPlay'], tab.id));
                    }
                });
            });
        }
        if (tabId === state.tabIds.panel && !state.closingAllWindows) {
            logKantuClosing();
        }
    });

    Ext.tabs.onUpdated.addListener((tabId: number, changeInfo: any, tab: any) => {
        if (!tab.active) return;

        isTabActiveAndFocused(tabId).then((isFocused) => {
            if (!isFocused) return;
            return notifyPanelAboutActiveTab(tabId);
        });
    });



    const getCalculatedShowSidePanelValue = (config: any) => {
        let value = false;
        if (config) {
            if (config.oneTimeShowSidePanel && [true, false].includes(config.oneTimeShowSidePanel)) {
                value = config.oneTimeShowSidePanel;
            } else {
                value = config.showSidePanel;
            }
        }
        return value;
    };

    if (isFirefox()) {
        storage.addListener(([storage]: any) => {
            if (storage.key === 'config') {
                console.log('config changed:>> ', storage);
                if (storage.newValue.oneTimeShowSidePanel !== storage.oldValue.oneTimeShowSidePanel && [true, false].includes(storage.newValue.oneTimeShowSidePanel)) {
                    showSidePanel = storage.newValue.oneTimeShowSidePanel;
                } else {
                    showSidePanel = storage.newValue.showSidePanel;
                    getState().then((state: any) => {
                        isSidePanelOpen = state.tabIds.panel === SIDEPANEL_TAB_ID;
                    });
                }
            }
        });
    }

    // these three variables are used for the feature of opening side panel on icon click according to the settings stored in storage->config
    // using async functions to get the active tab id, and the showSidePanel variable from storage config will cause an error.
    // https://stackoverflow.com/questions/77213045/error-sidepanel-open-may-only-be-called-in-response-to-a-user-gesture-re
    let showSidePanel: boolean, isSidePanelOpen: boolean, keepAliveInterval: any;

    // keep service worker alive only when side panel is set to open on icon click
    const manageKeepSWAlive = async () => {
        return storage.get('config').then((config) => {
            // because we cannot read this storage value between user clicking extension icon and calling Ext.sidePanel.open
            showSidePanel = getCalculatedShowSidePanelValue(config); // config && config.showSidePanel
            if (showSidePanel && !keepAliveInterval) {
                keepAliveInterval = setInterval(() => {
                    Ext.runtime.getPlatformInfo();
                }, 25e3);
            } else if (!showSidePanel && keepAliveInterval) {
                clearInterval(keepAliveInterval);
                keepAliveInterval = null;
            }
        });
    };

    Ext.windows.onFocusChanged.addListener((windowId: number) => {
        manageKeepSWAlive();

        Ext.tabs.query({ windowId, active: true }).then((tabs: any) => {
            // lw:排除popup
            tabs = tabs.filter((tab: any) => tab.url !== Ext.runtime.getURL('panel.html'));
            if (tabs.length === 0) return;
            askTab(tabs[0].id, 'TAB_ACTIVATED', {})
        });
    });

    Ext.runtime.onStartup.addListener(async () => {
        manageKeepSWAlive();
    });

    // Note: set the activated tab as the one to play
    Ext.tabs.onActivated.addListener(async (activeInfo: any) => {
        manageKeepSWAlive();

        const [state, tab] = await Promise.all([getState(), Ext.tabs.get(activeInfo.tabId)]);

        if (activeInfo.tabId === state.tabIds.panel || tab.url.indexOf('chrome://extensions/') !== -1 || tab.url.indexOf(Ext.runtime.getURL('')) !== -1) {
            return;
        }

        // Just in case we add panel tabId into it before we know it's a panel
        await updateState((state) => ({
            ...state,
            tabIds: {
                ...state.tabIds,
                lastActivated: state.tabIds.lastActivated
                    .concat(activeInfo.tabId)
                    .filter((tabId) => tabId !== state.tabIds.panel)
                    .slice(-2),
            },
        }));
        askTab(activeInfo.tabId, 'TAB_ACTIVATED', {})

        notifyPanelAboutActiveTab(activeInfo.tabId);

        switch (state.status) {
            case CONSTANT.APP_STATUS.NORMAL:
                if (activeInfo.tabId === state.tabIds.panel) {
                    return;
                }

                const updateTabIds = () => {
                    Ext.tabs.get(activeInfo.tabId).then((tab: any) => {
                        if (tab.url.indexOf(Ext.runtime.getURL('')) !== -1) return;
                        if (activeInfo.tabId === state.tabIds.panel) return;

                        log('in tab activated, set toPlay to ', activeInfo);

                        return updateState((state) => ({
                            ...state,
                            tabIds: {
                                ...state.tabIds,
                                lastPlay: state.tabIds.toPlay,
                                toPlay: activeInfo.tabId,
                                firstPlay: activeInfo.tabId,
                            },
                        }));
                    });
                };

                // Note: In Firefox, without this delay of 100ms, `tab.url` will still be 'about:config'
                // so have to wait for the url to take effect
                if (isFirefox()) {
                    setTimeout(updateTabIds, 100);
                } else {
                    updateTabIds();
                }

                break;

            case CONSTANT.APP_STATUS.RECORDER: {
                // Note: three things to do when switch tab in recording
                // 1. set the new tab to RECORDING status,
                // 2. and the original one back to NORMAL status
                // 3. commit a `selectWindow` command
                //
                // Have to wait for the new tab establish connection with background

                askTab(activeInfo.tabId, 'SET_STATUS', { status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING, }).then(() => {
                    // Note: set the original tab to NORMAL status
                    // only if the new tab is set to RECORDING status
                    return Promise.resolve()
                    //.then(() => {
                    //     askRecordTab('SET_STATUS', {
                    //         status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL,
                    //     });
                    // });
                })
                    .then(() => getState())
                    .then((state) => {
                        // Note: get window locator & update recording tab
                        const oldTabId = state.tabIds.firstRecord;
                        const newTabId = activeInfo.tabId;

                        return Promise.all([Ext.tabs.get(oldTabId), Ext.tabs.get(newTabId)]).then(async ([oldTab, newTab]) => {
                            const result = [];

                            // update recording tab
                            await updateState(setIn(['tabIds', 'toRecord'], activeInfo.tabId));

                            if (oldTab.windowId === newTab.windowId) {
                                result.push(`tab=${newTab.index - oldTab.index}`);
                            }

                            result.push(`title=${newTab.title}`);

                            return {
                                target: result[0],
                                targetOptions: result,
                            };
                        });
                    })
                    .then((data) => {
                        // Note: commit the `selectWindow` command
                        const command = {
                            cmd: 'selectWindow',
                            ...data,
                        };

                        return Promise.resolve()
                            .then(() => askPanel('RECORD_ADD_COMMAND', command))
                            .then((shouldNotify) => {
                                if (shouldNotify) {
                                    notifyRecordCommand(command);
                                }
                            });
                    })
                    .catch((e) => {
                        log.error(e.stack);
                    });

                break;
            }
        }
    });

    Ext.runtime.onConnect.addListener(function (port: any) {
        if (port.name === SIDEPANEL_PORT_NAME) {
            console.log('side panel connected');
            isSidePanelOpen = true;
            port.onDisconnect.addListener(async () => {
                console.log('side panel disconnected');
                isSidePanelOpen = false;
            });
        }
    });

    Ext.downloads.onChanged.addListener(function (e: any) {
        let downloadDelta = e;
        Promise.resolve().then(() => {
            if (typeof downloadDelta.state !== 'undefined') {
                if (downloadDelta.state.current === 'complete') {
                    Ext.downloads.search({ id: downloadDelta.id }, function (downloadItems: any) {
                        if (downloadItems && downloadItems.length > 0) {
                            console.log('Downloaded file name111: ' + downloadItems[0].filename);
                            let downloadItem = { filename: downloadItems[0].filename };
                            askPanel('DOWNLOAD_COMPLETE', downloadItem);
                        }
                    });
                    storage.get('config').then(async (config = {}) => {
                        const state = await getState();
                        if (config.cvScope === 'browser' && state.status == 'PLAYER') {
                            setTimeout(function () {
                                Ext.downloads.erase({ state: 'complete' });
                            }, 2000);
                        }
                    });
                }
            }
        });
    });
};

// usage:
// 1. set tabId for inspector:  `setInspectorTabId(someTabId)`
// 2. clear tabId for inspector: `setInspectorTabId(null, true)`
const setInspectorTabId = async (tabId: number | null, shouldRemove: boolean, noNotify: boolean) => {
    const state = await getState();
    const lastInspect = state.tabIds.toInspect;

    await updateState((state) => ({
        ...state,
        tabIds: {
            ...state.tabIds,
            lastInspect,
            toInspect: tabId,
        },
    }));

    if (shouldRemove) {
        if (lastInspect) {
            if (noNotify) {
                return Promise.resolve(true);
            }
            return askTab(lastInspect, 'STOP_INSPECTING', {}).catch((e) => log(e.stack));
        }

        return Promise.resolve(true);
    }
};


// Processor for all message background could receive
// All messages from panel starts with 'PANEL_'
// All messages from content script starts with 'CS_'
const onRequest = async (cmd: string, args: any) => {
    const state = await getState();

    if (cmd !== 'CS_ACTIVATE_ME' && cmd !== 'TIMEOUT') {
        log('onAsk', cmd, args);
    }

    switch (cmd) {
        // Mark the tab as panel.
        case 'I_AM_PANEL':
            // 0.5s delay to make sure toPlay is set in tabs.onActivated event
            await delay(() => { }, 500);
            // When panel window is opened, it's always in normal mode,
            // so make sure contextMenus for record mode are removed

            let isSidePanel = args.sender.tab?.id === SIDEPANEL_TAB_ID || args.sender.url === `chrome-extension://${Ext.runtime.id}/sidepanel.html` || args.sender.url.match(/moz-extension:\/\/[a-z0-9-]+\/sidepanel.html/);
            let panelTabId = isSidePanel ? SIDEPANEL_TAB_ID : args.sender.tab.id;
            await updateState(setIn(['tabIds', 'panel'], panelTabId));
            if (!isSidePanel) {
                updateState(setIn(['tabIds', 'lastPanelWindow'], panelTabId));
            }

            getContextMenuService().destroyMenus();

            // Note: when the panel first open first, it could be marked as the tab to play
            // That's something we don't want to happen
            if (args.sender.tab && args.sender.tab.id && state.tabIds.toPlay === args.sender.tab.id) {
                await updateState((state) => ({
                    ...state,
                    tabIds: {
                        ...state.tabIds,
                        toPlay: state.tabIds.lastPlay,
                        firstPlay: state.tabIds.lastPlay,
                        lastActivated: state.tabIds.lastActivated.filter((id) => id !== args.sender.tab.id),
                    },
                }));
            }

            return true;


        case 'PANEL_CAPTURE_VISIBLE_TAB': {
            return Ext.tabs.captureVisibleTab(args.windowId, args.options).catch((e: any) => {
                console.log('captureVisibleTab e:>>', e);
                if (e == 'Error: Missing activeTab permission') {
                    throw new Error('Error E144: Screenshot permission issue. To fix, please reload extension.' + 'To do so, go to extension settings and turn the blue switch OFF and then ON again.');
                }
                throw e;
            });
        }

        case 'PANEL_SET_PROXY': {
            return setProxy(args.proxy).then(() => true);
        }

        case 'PANEL_GET_PROXY': {
            return getProxyManager().getProxy();
        }

        case 'PANEL_TIME_FOR_BACKUP':
            return isTimeToBackup().then((obj) => obj.timeout);

        case 'PANEL_LOG':
            return

        // panel->bg->playTab
        case 'PANEL_CALL_PLAY_TAB': {
            const { payload } = args;

            return Promise.resolve().then(() => {
                return askPlayTab(payload.command, payload.args);
            });
        }

        case 'PANEL_CS_IPC_READY': {
            const { tabId, timeout } = args;
            return askTab(tabId, 'IPC_READY', {}, timeout).then(() => true);
        }

        case 'PANEL_HAS_PENDING_DOWNLOAD': {
            return getDownloadMan().hasPendingDownload();
        }

        case 'PANEL_WAIT_FOR_ANY_DOWNLOAD': {
            return getDownloadMan()
                .waitForDownloadIfAny()
                .then(() => true);
        }

        case 'PANEL_START_RECORDING': {
            log('Start to record...');

            askRecordTab('SET_STATUS', {
                status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING,
            });

            await updateState({ status: CONSTANT.APP_STATUS.RECORDER });

            setInspectorTabId(null, true, false);
            toggleRecordingBadge(true, {});

            const menuInfos = [
                {
                    id: 'verifyText',
                    title: 'Verify Text',
                    contexts: ['page', 'selection'],
                },
                {
                    id: 'verifyTitle',
                    title: 'Verify Title',
                    contexts: ['page', 'selection'],
                },
                {
                    id: 'assertText',
                    title: 'Assert Text',
                    contexts: ['page', 'selection'],
                },
                {
                    id: 'assertTitle',
                    title: 'Assert Title',
                    contexts: ['page', 'selection'],
                },
            ].map((item) => ({
                ...item,
                onclick: () => {
                    () => askRecordTab('CONTEXT_MENU_IN_RECORDING', { command: item.id });
                },
            }));

            getContextMenuService().createMenus(menuInfos as any);

            // const list = state.tabIds.lastActivated.filter((id: any) => id !== state.tabIds.panel);
            // const lastActivatedTabId = list[list.length - 1];
            const tab: any = await getPlayTab()
            // if (lastActivatedTabId) {
            activateTab(tab.id, true).catch((e) => {
                log.warn(`Failed to activate current tab: ${e.message}`);
            });
            // }

            return true;
        }

        case 'PANEL_STOP_RECORDING':
            log('Stop recording...');

            getContextMenuService().destroyMenus();

            askRecordTab('SET_STATUS', {
                status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL,
            });


            await updateState((state) => ({
                ...state,
                status: CONSTANT.APP_STATUS.NORMAL,
                tabIds: {
                    ...state.tabIds,
                    toRecord: null,
                    firstRecord: null,
                    lastRecord: state.tabIds.toRecord,
                },
            }));

            toggleRecordingBadge(false, {});
            return true;

        case 'PANEL_TRY_TO_RECORD_OPEN_COMMAND': {
            if (state.status !== CONSTANT.APP_STATUS.RECORDER) {
                throw new Error('E215: Not in recorder mode');
            }

            // Well, `getPlayTab` is actually 'get current active tab'
            return getPlayTab().then(async (tab: any) => {
                log('PANEL_TRY_TO_RECORD_OPEN_COMMAND', tab);

                if (!/^(https?:|file:)/.test(tab.url)) {
                    throw new Error('E216: Not a valid url to record as open command');
                }

                await updateState((state) => ({
                    ...state,
                    tabIds: {
                        ...state.tabIds,
                        toRecord: tab.id,
                        firstRecord: tab.id,
                    },
                }));

                Promise.resolve().then(() => {
                    const command = {
                        cmd: 'open',
                        target: tab.url,
                    };

                    askPanel('RECORD_ADD_COMMAND', command);
                    notifyRecordCommand(command);
                });

                return true;
            });
        }
        // 开始捕获元素
        case 'PANEL_START_INSPECTING': {
            log('start to inspect...');
            toggleInspectingBadge(true, {});

            // if (state.tabIds.toPlay) {
            //     // 激活页签
            //     activateTab(state.tabIds.toPlay, true);
            // }
            // 更新状态
            await updateState({ status: CONSTANT.APP_STATUS.INSPECTOR });
            return true;
        }
        // 停止捕获元素
        case 'PANEL_STOP_INSPECTING': {
            log('start to inspect...');
            await updateState({ status: CONSTANT.APP_STATUS.NORMAL });

            toggleInspectingBadge(false, {});
            return setInspectorTabId(null, true, false);
        }
        // 开始运行
        case 'PANEL_START_PLAYING': {
            log('start to play...');
            await updateState({
                status: CONSTANT.APP_STATUS.PLAYER,
                pendingPlayingTab: false,
                xClickNeedCalibrationInfo: null,
            });

            storage.get('config').then(async (config = {}) => {
                const state = await getState();
                if (config.cvScope === 'browser' && state.status == 'PLAYER') {
                    setTimeout(function () {
                        Ext.downloads.erase({ state: 'complete' });
                    }, 2000);
                }
            });

            setInspectorTabId(null, true, false);
            togglePlayingBadge(true, {});
            // Note: reset download manager to clear any previous downloads
            getDownloadMan().reset();
            // Re-check log service to see if xfile is ready to write log

            if (state.timer) clearInterval(state.timer);

            return true;

        }
        // 心跳
        case 'PANEL_HEART_BEAT': {
            return getState('heartBeatSecret').then((secret = 0) => secret);
        }
        // 停止运行
        case 'PANEL_STOP_PLAYING': {
            // IMPORTANT: make updating status to normal the first thing in this branch,
            // otherwise it might accidently overwrite the status of following PANEL_START_PLAYING
            await updateState((state) => ({
                ...state,
                status: CONSTANT.APP_STATUS.NORMAL,
                tabIds: {
                    ...state.tabIds,
                    // Note: reset firstPlay to current toPlay when stopped playing
                    // userful for playing loop (reset firstPlay after each loop)
                    firstPlay: state.tabIds.toPlay,
                    // reset lastPlay here is useful for ContinueInLastUsedTab
                    lastPlay: state.tabIds.toPlay,
                },
            }));

            // Note: let cs know that it should exit playing mode
            askTab(state.tabIds.toPlay, 'SET_STATUS', { status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL }, CONSTANT.CS_IPC_TIMEOUT);


            // lw todo: 通知启动页，执行结果及输出变量
            // 运行时，记录启动页
            // 给启动页发送消息
            // 启动页接收到消息，再发送消息给top window

            askTab(state.tabIds.toInvoke, 'INVOKE_RESPONSE', {
                data:
                    args.reason === 'COMPLETE'
                        ? {
                            variables: args.variables,
                            reason: args.reason,
                            opts: args.opts,
                            extra: args.extra,
                        }
                        : null,
                status: args.reason === 'COMPLETE' ? 1 : 0,
                code: args.reason === 'COMPLETE' ? 0 : -1,
                msg: args.reason === 'COMPLETE' ? '执行成功' : args.opts?.msg || '未知原因',
                stack: args.opts?.stack || null,
            });

            togglePlayingBadge(false, {});

            // Note: reset download manager to clear any previous downloads
            getDownloadMan().reset();

            if (state.timer) clearInterval(state.timer);

            return true;
        }

        // corresponding to the 'find' functionality on dashboard panel
        // It will find either the last play tab or record tab to look for the passed in locator
        // 高亮元素
        case 'PANEL_HIGHLIGHT_DOM': {


            return askPlayTab('HIGHLIGHT_DOM', {
                locator: args.locator,
                cmd: args.cmd,
            })

            return Promise.all([
                Promise.resolve()
                    .then(() => ({ ipc: askRecordTab, type: 'record' }))
                    .catch(() => null),
                Promise.resolve()
                    .then(() => ({ ipc: askPlayTab, type: 'play' }))
                    .catch(() => null),
            ])
                .then((tuple) => {
                    if (!tuple[0] && !tuple[1]) {
                        throw new Error('E218: No where to look for the dom');
                    }

                    return tuple.filter((x) => !!x);
                })
                .then((list) => {
                    return Promise.all(
                        list.map(({ ipc, type }) => {
                            return ipc('FIND_DOM', { locator: args.locator }).then((result) => ({ result, type, ipc }));
                        })
                    );
                })
                .then(async (list) => {
                    const foundedList = list.filter((x) => x.result);

                    if (foundedList.length === 0) {
                        throw new Error('E219: DOM not found');
                    }

                    const item: any = foundedList.length === 2 ? foundedList.find((item) => item.type === args.lastOperation) : foundedList[0];

                    const state = await getState();
                    const tabId = state.tabIds[item.type === 'record' ? 'lastRecord' : 'toPlay'];

                    return activateTab(tabId, true).then(() =>
                        item.ipc('HIGHLIGHT_DOM', {
                            locator: args.locator,
                            cmd: args.cmd,
                        })
                    );
                });
        }
        // 高亮区域
        case 'PANEL_HIGHLIGHT_RECT': {
            return Promise.resolve().then(() => askPlayTab('HIGHLIGHT_RECT', args, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_HIGHLIGHT_X': {
            return Promise.resolve().then(() => askPlayTab('HIGHLIGHT_X', args, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_HIGHLIGHT_RECTS': {
            console.log('PANEL_HIGHLIGHT_RECTS:>>', args);
            return Promise.resolve().then(() => askPlayTab('HIGHLIGHT_RECTS', args, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_HIGHLIGHT_DESKTOP_RECTS': {
            return
        }

        case 'PANEL_HIGHLIGHT_DESKTOP_X': {
            return
        }

        case 'PANEL_HIGHLIGHT_OCR_MATCHES': {
            return Promise.resolve().then(() => askPlayTab('HIGHLIGHT_OCR_MATCHES', args, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_CLEAR_OCR_MATCHES_ON_PLAYING_PAGE': {
            return Promise.resolve().then(() => {
                return Promise.all([askPlayTab('CLEAR_VISION_RECTS', {}, CONSTANT.CS_IPC_TIMEOUT), askPlayTab('CLEAR_OCR_MATCHES', {}, CONSTANT.CS_IPC_TIMEOUT)]);
            });
        }

        case 'PANEL_RESIZE_WINDOW': {
            if (!state.tabIds.panel) {
                throw new Error('E220: Panel not available');
            }

            return Ext.tabs.get(state.tabIds.panel).then((tab: any) => {
                return Ext.windows.update(
                    tab.windowId,
                    pick(['width', 'height'], {
                        ...args.size,
                        width: args.size.width,
                        height: args.size.height,
                        state: 'normal',
                    })
                );
            });
        }

        case 'PANEL_UPDATE_BADGE': {
            const dict: any = {
                play: togglePlayingBadge,
                record: toggleRecordingBadge,
                inspect: toggleInspectingBadge,
            };
            const fn = dict[args.type];

            if (!fn) {
                throw new Error(`E221: unknown type for updating badge, '${args.type}'`);
            }

            return fn(!args.clear, args);
        }

        case 'PANEL_NOTIFY_AUTO_PAUSE': {
            notifyAutoPause();
            return true;
        }

        case 'PANEL_NOTIFY_BREAKPOINT': {
            notifyBreakpoint();
            return true;
        }

        case 'PANEL_NOTIFY_ECHO': {
            notifyEcho(args.text);
            return true;
        }

        case 'PANEL_CLOSE_ALL_WINDOWS': {
            await updateState({ closingAllWindows: true });

            return logKantuClosing()
                .catch((e: any) => {
                    log.warn('E222: Error in log => RPA closing: ', e.message);
                })
                .then(() => {
                    closeAllWindows();
                    return true;
                });
        }

        case 'PANEL_CURRENT_PLAY_TAB_INFO': {
            return getPlayTab().then((tab) => {
                return {
                    url: tab.url,
                    title: tab.title,
                };
            });
        }
        // lw:最小化除IDE以外的窗口
        case 'PANEL_MINIMIZE_ALL_WINDOWS_BUT_PANEL': {
            const pPanelTab = !state.tabIds.panel ? Promise.resolve() : Ext.tabs.get(state.tabIds.panel);
            const pAllWindows = Ext.windows.getAll();

            return Promise.all([pPanelTab, pAllWindows])
                .then(([tab, wins]) => {
                    const list = !tab ? wins : wins.filter((win: any) => win.id !== tab.windowId);
                    return Promise.all(list.map((win: any) => Ext.windows.update(win.id, { state: 'minimized' })));
                })
                .then(() => delay(() => true, 500));
        }
        // lw:关闭除了调用窗口和IDE外的其他窗口
        case 'PANEL_CLOSE_ALL_WINDOWS_BUT_INVOKE': {
            // p开头代表返回promise
            const pPanelTab = !state.tabIds.panel ? Promise.resolve() : Ext.tabs.get(state.tabIds.panel);
            const pAllWindows = Ext.windows.getAll();
            const pInvokeTab = !state.tabIds.toInvoke ? Promise.resolve() : Ext.tabs.get(state.tabIds.toInvoke);

            return Promise.all([pPanelTab, pAllWindows, pInvokeTab])
                .then(([tab, wins, invokeTab]) => {
                    const list = !tab || !invokeTab ? [] : wins.filter((win: any) => win.id !== tab.windowId && win.id !== invokeTab.windowId);
                    return Promise.all(list.map((win: any) => Ext.windows.remove(win.id)));
                })
                .then(() => delay(() => true, 500));
        }
        // lw:最小化IDE
        case 'PANEL_MINIMIZE': {
            // p开头代表返回promise
            const pPanelTab = !state.tabIds.panel ? Promise.resolve() : Ext.tabs.get(state.tabIds.panel);
            const pAllWindows = Ext.windows.getAll();

            return Promise.all([pPanelTab, pAllWindows])
                .then(([tab, wins]) => {
                    const list = !tab ? [] : wins.filter((win: any) => win.id === tab.windowId);
                    return Promise.all(list.map((win: any) => Ext.windows.update(win.id, { state: 'minimized' })));
                })
                .then(() => delay(() => true, 500));
        }
        // lw:最小化所有窗口
        case 'PANEL_MINIMIZE_ALL_WINDOWS': {
            return Ext.windows.getAll().then((wins: any) => {
                return Promise.all(wins.map((win: any) => Ext.windows.update(win.id, { state: 'minimized' }))).then(() => delay(() => true, 500));
            });
        }

        case 'PANEL_BRING_PANEL_TO_FOREGROUND': {
            return showPanelWindow({}).then(() => true);
        }

        case 'PANEL_BRING_PLAYING_WINDOW_TO_FOREGROUND': {
            return getPlayTab()
                .then((tab: any) => activateTab(tab.id, true))
                .catch(() => showPanelWindow({}))
                .then(() => true);
        }

        /*    case 'PANEL_IS_PLAYING_WINDOW_IN_FOREGROUND': {
      return getPlayTab()
      .then(tab => {
        if (!tab) return false
    
        return Ext.windows.get(tab.windowId)
        .then(win => !!win.focused)
      })
    }
    */

        case 'PANEL_RESIZE_PLAY_TAB': {
            return getPlayTab().then((tab: any) => resizeViewportOfTab(tab.id, args.viewportSize, args.screenAvailableRect));
        }

        case 'PANEL_GET_WINDOW_SIZE_OF_PLAY_TAB': {
            return getPlayTab().then((tab: any) => {
                console.log('PANEL_GET_WINDOW_SIZE_OF_PLAY_TAB tab:>> ', tab);
                return getWindowSize(tab.windowId);
            });
        }

        case 'PANEL_SELECT_AREA_ON_CURRENT_PAGE': {
            return Promise.resolve().then(() => {
                // activateTab(state.tabIds.toPlay, true);
                return askPlayTab('SELECT_SCREEN_AREA', {});
            })
                .catch((e: any) => {
                    log.error(e.stack);
                    throw new Error('E205: Not able to take screenshot on the current tab');
                });
        }

        case 'PANEL_CLEAR_VISION_RECTS_ON_PLAYING_PAGE': {
            return Promise.resolve().then(() => {
                return Promise.all([askPlayTab('CLEAR_VISION_RECTS', {}, CONSTANT.CS_IPC_TIMEOUT), askPlayTab('CLEAR_OCR_MATCHES', {}, CONSTANT.CS_IPC_TIMEOUT)]);
            });
        }

        case 'PANEL_HIDE_VISION_HIGHLIGHT': {
            return Promise.resolve().then(() => askPlayTab('HIDE_VISION_RECTS', {}, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_SHOW_VISION_HIGHLIGHT': {
            return Promise.resolve().then(() => askPlayTab('SHOW_VISION_RECTS', {}, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_SCREENSHOT_PAGE_INFO': {
            return Promise.resolve().then(() => askPlayTab('SCREENSHOT_PAGE_INFO', {}, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_TOGGLE_HIGHLIGHT_VIEWPORT': {
            return Promise.resolve().then(() => askPlayTab('TOGGLE_HIGHLIGHT_VIEWPORT', args, CONSTANT.CS_IPC_TIMEOUT));
        }

        case 'PANEL_DISABLE_DOWNLOAD_BAR': {
            // Ext.downloads.setShelfEnabled(false)
            Ext.downloads.setUiOptions({ enabled: false });
            return delay(() => true, 1000);
        }

        case 'PANEL_ENABLE_DOWNLOAD_BAR': {
            // Ext.downloads.setShelfEnabled(true)
            Ext.downloads.setUiOptions({ enabled: true });
            return delay(() => true, 1000);
        }

        case 'PANEL_GET_VIEWPORT_RECT_IN_SCREEN': {
            return Promise.all([
                Promise.resolve(),
                getPlayTab().then((tab: any) => {
                    return Ext.tabs.getZoom(tab.id);
                }),
            ]).then(([_, zoom]) => {
                return Promise.resolve().then(() => askPlayTab('GET_VIEWPORT_RECT_IN_SCREEN', { zoom }));
            });
        }

        case 'PANEL_XCLICK_NEED_CALIBRATION': {
            const last = state.xClickNeedCalibrationInfo;
            const getWindowInfo = (win: any, tabId: any) => ({
                id: win.id,
                top: win.top,
                left: win.left,
                width: win.width,
                height: win.height,
                activeTabId: tabId,
            });
            const isWindowInfoEqual = (a: any, b: any) => {
                return and(...'id, top, left, width, height, activeTabId'.split(/,\s*/g).map((key) => a[key] === b[key]));
            };
            // Note: we take every request as it will do calibration
            // and next request should get `false` (no need for more calibration, unless there are window change or window resize)
            return getPlayTab().then((tab: any) => {
                if (!tab) throw new Error('E206: no play tab found for calibration');

                return Ext.windows.get(tab.windowId).then(async (win: any) => {
                    const winInfo = getWindowInfo(win, tab.id);

                    log('CALIBRATION NEED???', last, winInfo);

                    // Note: cache last value
                    await updateState({ xClickNeedCalibrationInfo: winInfo });

                    return !isWindowInfoEqual(winInfo, last || {});
                });
            });
        }

        case 'PANEL_CLOSE_CURRENT_TAB_AND_SWITCH_TO_LAST_PLAYED': {
            return getPlayTab().then((currentTab: any) => {
                return Ext.windows.get(currentTab.windowId, { populate: true }).then(async (win: any) => {
                    if (win.tabs.length < 2) return true;

                    const index = win.tabs.findIndex((tab: any) => tab.id === currentTab.id);
                    const prevIndex = (index - 1 + win.tabs.length) % win.tabs.length;
                    const prevTab = win.tabs[prevIndex];
                    const state = await getState();

                    const pNextTab = (() => {
                        if (state.tabIds.lastPlay) {
                            return Ext.tabs.get(state.tabIds.lastPlay).catch(() => prevTab);
                        } else {
                            return Promise.resolve(prevTab);
                        }
                    })();

                    if (currentTab.id == state.tabIds.lastPlay) {
                        return Ext.tabs
                            .get(currentTab.id)
                            .then(() => pNextTab)
                            .then((nextTab: any) => activateTab(nextTab.id))
                            .then(() => delay(() => { }, 500))
                            .then(() => true);
                    } else {
                        return (
                            Ext.tabs
                                .remove(currentTab.id)
                                .then(() => pNextTab)
                                .then((nextTab: any) => activateTab(nextTab.id))
                                // Note: add this delay to avoid Error #101
                                // looks like when the pc is quick enough, there are chances
                                // that next macro run fails to find the tab for replay
                                .then(() => delay(() => { }, 500))
                                .then(() => true)
                        );
                    }
                });
            });
        }

        case 'PANEL_OPEN_IN_SIDEPANEL': {
            // we cannot open sidepanel using ipc
        }

        case 'CS_LOAD_URL': {
            const tabId = args.sender.tab.id;
            const url = args.url;
            const cmd = args.cmd;

            return getTab(tabId).then((tab) => {
                const finalUrl = (() => {
                    try {
                        const u = new URL(url, tab.url);
                        return u.toString();
                    } catch (e) {
                        return url;
                    }
                })();

                return updateUrlForTab(tabId, finalUrl, cmd).then(() => true);
            });
        }

        case 'CS_STORE_SCREENSHOT_IN_SELECTION': {
            const { rect, devicePixelRatio, fileName } = args;
            return getPlayTab().then((tab: any) => {
                return askPanel('STORE_SCREENSHOT_IN_SELECTION', {
                    rect,
                    tabId: tab.id,
                    windowId: tab.windowId,
                    fileName,
                    devicePixelRatio,
                });
            });
        }

        case 'CS_SCREEN_AREA_SELECTED': {
            const { rect, devicePixelRatio } = args;

            return getPlayTab().then((tab: any) => {
                log('CS_SCREEN_AREA_SELECTED', rect, devicePixelRatio, tab.id);
                return askPanel('SCREEN_AREA_SELECTED', {
                    windowId: tab.windowId,
                    rect,
                    tabId: tab.id,
                    devicePixelRatio,
                })
            })



        }

        case 'CS_DONE_INSPECTING':
            log('done inspecting...');

            await updateState({ status: CONSTANT.APP_STATUS.NORMAL });

            toggleInspectingBadge(false, {});
            setInspectorTabId(null, true, true);
            // activateTab(state.tabIds.panel, true);

            return Promise.resolve().then(() => {
                return askPanel('INSPECT_RESULT', args);
            });

        // It's used for inspecting. The first tab which sends a CS_ACTIVATE_ME event
        // on mouse over event will be the one for us to inspect
        case 'CS_ACTIVATE_ME':
            console.log(state.status, args.sender.tab.id)
            switch (state.status) {

                case CONSTANT.APP_STATUS.INSPECTOR:
                    if (!state.tabIds.toInspect) {
                        const tabId = args.sender.tab.id;
                        await updateState(setIn(['tabIds', 'toInspect'], tabId));

                        setTimeout(() => {
                            askTab(tabId, 'SET_STATUS', {
                                status: CONSTANT.CONTENT_SCRIPT_STATUS.INSPECTING,
                            });
                        }, 0);

                        return true;
                    }
                    break;
                case CONSTANT.APP_STATUS.RECORDER:
                    const tabId = args.sender.tab.id;
                    setTimeout(() => {
                        askTab(tabId, 'SET_STATUS', {
                            status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING,
                        });
                    }, 0);
                    return true;
            }
            return false;

        case 'CS_RECORD_ADD_COMMAND': {
            const pullbackTimeout = 1000;
            let isFirst = false;

            if (state.status !== CONSTANT.APP_STATUS.RECORDER) {
                return false;
            }

            if (!state.tabIds.toRecord) {
                isFirst = true;

                await updateState((state) => ({
                    ...state,
                    tabIds: {
                        ...state.tabIds,
                        toRecord: args.sender.tab.id,
                        firstRecord: args.sender.tab.id,
                    },
                }));
            }

            if (state.tabIds.toRecord !== args.sender.tab.id) {
                return false;
            }

            // Note: if receive a pullback cmd, we need to set the flag,
            // and strip Wait from any xxxAndWait command
            if (args.cmd === 'pullback') {
                updateState({ pullback: true });
                setTimeout(() => updateState({ pullback: false }), pullbackTimeout * 2);
                return false;
            }

            setTimeout(() => {
                askTab(state.tabIds.toRecord, 'SET_STATUS', {
                    status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING,
                });
            }, 0);

            return delay(() => { }, pullbackTimeout)
                .then(async () => {
                    if (isFirst) {
                        askPanel('RECORD_ADD_COMMAND', {
                            cmd: 'open',
                            target: args.url,
                        });
                    }

                    // Note: remove AndWait from commands if we got a pullback
                    const state = await getState();

                    if (state.pullback) {
                        args.cmd = args.cmd.replace('AndWait', '');
                        await updateState({ pullback: false });
                    }

                    return askPanel('RECORD_ADD_COMMAND', args);
                })
                .then(() => Promise.all([storage.get('config'), getState()]))
                .then(([config, state]: any) => {
                    console.log(config, 'recordNotification')
                    // if (config?.recordNotification && state.status === CONSTANT.APP_STATUS.RECORDER) {
                    notifyRecordCommand(args);
                    // }
                })
                .then(() => true);
        }

        case 'PANEL_CLOSE_OTHER_TABS': {
            const tabId = state.tabIds.toPlay;

            return Ext.tabs
                .get(tabId)
                .then((tab: any) => {
                    return Ext.tabs
                        .query({ windowId: tab.windowId })
                        .then((tabs: any) => tabs.filter((t: any) => t.id !== tabId))
                        .then((tabs: any) => Ext.tabs.remove(tabs.map((t: any) => t.id)));
                })
                .then(() => true);
        }

        case 'PANEL_CLOSE_CURRENT_TAB': {
            const tabId = state.tabIds.toPlay;

            // Note: must disable heart beat check here, since the heart beat of current tab is destined to be lost
            // The following two states are dedicated to this close tab task
            await updateState({
                disableHeartBeat: true,
                pendingPlayingTab: true,
            });

            const closeTabAndGetNextTabOnWindow = (winId: any) => {
                return Ext.tabs.remove(tabId).then(() => delay(() => getCurrentTab(winId), 1000));
            };

            const withKantuWindowMinimized = (fn: any) => {
                const getPanelWinId = () => Ext.tabs.get(state.tabIds.panel).then((tab: any) => tab.windowId);
                const minimize = () => getPanelWinId().then((winId: any) => Ext.windows.update(winId, { state: 'minimized' }));
                const restore = () => getPanelWinId().then((winId: any) => Ext.windows.update(winId, { state: 'normal' }));

                return minimize()
                    .then(() => delay(() => { }, 1000))
                    .then(fn)
                    .then(
                        (data: any) => {
                            restore();
                            return data;
                        },
                        (e: any) => {
                            restore();
                            throw e;
                        }
                    );
            };

            const closeAndGetNextTab = () => {
                return Ext.tabs
                    .get(tabId)
                    .then((tab: any) => {
                        // Note: If the current tab is the only tab in its window, we won't know which one is the next focused window,
                        // if Kantu window happens to be on the top. In this case, we need to focus on the tab
                        // that is going to be closed first
                        return Ext.windows.get(tab.windowId, { populate: true }).then((win: any) => {
                            if (win.tabs.length !== 1) {
                                return closeTabAndGetNextTabOnWindow(tab.windowId);
                            }

                            // If Kantu window is now on top, try to pick the next one (by minimize Kantu window)
                            // Otherwise pick the current tab will be fine
                            return getCurrentTab().then((tab) => {
                                if (tab && tab.id !== state.tabIds.panel) {
                                    return closeTabAndGetNextTabOnWindow(tab.windowId).then((tab: any) => {
                                        if (tab && tab.id === state.tabIds.panel) {
                                            return withKantuWindowMinimized(getCurrentTab);
                                        }
                                        return tab;
                                    });
                                }

                                return withKantuWindowMinimized(closeTabAndGetNextTabOnWindow);
                            });
                        });
                    })
                    .catch((e: any) => {
                        log.error(e);
                    });
            };

            const runWithTab = (pTab: any) => {
                return pTab
                    .then((tab: any) => {
                        log('getCurrentTab - ', tab);

                        const isValidTab = !!tab && !!tab.id;
                        const isPanelTab = isValidTab && tab.id === state.tabIds.panel;

                        return updateState(setIn(['tabIds', 'toPlay'], isValidTab && !isPanelTab ? tab.id : null));
                    })
                    .catch(() => { })
                    .then(() => {
                        // Note: should always reset pendingPlayingTab, no matter there is an error or not
                        log('resetting pendingPlayingTab');
                        return updateState({ pendingPlayingTab: false });
                    });
            };

            return runWithTab(closeAndGetNextTab()).then(() => true);
        }

        case 'PANEL_SELECT_WINDOW': {
            const oldTablId = state.tabIds.toPlay;
            const [type, locator] = splitIntoTwo('=', args.target);

            if (!locator) {
                throw new Error(`E207: invalid window locator, '${args.target}'`);
            }

            let pGetTabs;

            switch (type.toLowerCase()) {
                case 'title':
                    pGetTabs = Ext.tabs.query({ title: locator });
                    break;

                case 'tab': {
                    if (/^\s*open\s*$/i.test(locator)) {
                        pGetTabs = Ext.tabs
                            .get(state.tabIds.toPlay)
                            .then((tab: any) => Ext.tabs.create({ url: args.value, windowId: tab.windowId }))
                            .then((tab: any) => [tab]);
                    } else {
                        const offset = parseInt(locator, 10);

                        if (isNaN(offset)) {
                            throw new Error(`E208: Invalid tab offset, '${locator}'`);
                        }

                        pGetTabs = Ext.tabs.get(state.tabIds.firstPlay).then((tab: any) =>
                            Ext.tabs.query({
                                windowId: tab.windowId,
                                index: tab.index + offset,
                            })
                        );
                    }

                    break;
                }

                default:
                    throw new Error(`E209: window locator type '${type}' not supported`);
            }

            return pGetTabs
                .then((tabs: any) => {
                    if (tabs.length === 0) {
                        throw new Error(`E210: failed to find the tab with locator '${args.target}'`);
                    }
                    return tabs[0];
                })
                .then((tab: any) => {
                    log('selectWindow, got tab', tab);
                    // 等待页面加载完成
                    return askTab(tab.id, 'DOM_READY', {}, 30000).catch((e: any) => {
                        // args.target = 'tab=open' is a valid value, so this is commented out.
                        // if (/tab=\s*open\s*/i.test(args.target)) {
                        //   throw new Error('E211: To open a new tab, a valid URL is needed')
                        // }
                        throw new Error(`E225: DOM failed to be ready in 30sec.`);
                    })
                        .then((ipc) => {
                            log('selectWindow, got ipc', ipc);
                            const domReadyTimeout = 20000;
                            return askTab(tab.id, 'DOM_READY', {}, domReadyTimeout)
                                .catch((e) => {
                                    log.error(e);
                                    // most likely, ipc is not running properly in this tab
                                    throw new Error(`E226: DOM failed to be ready in ${domReadyTimeout} ms'`);
                                })
                                .then(() => {
                                    askTab(tab.id, 'SET_STATUS', {
                                        status: CONSTANT.CONTENT_SCRIPT_STATUS.PLAYING,
                                    });
                                    return true;
                                });
                        })
                        .catch((e) => {
                            console.error('DOM_READY Error ==:>> ', e);
                            throw e;
                        })
                        .then(() => {
                            // Note: set the original tab to NORMAL status
                            // only if the new tab is set to PLAYING status
                            log('selectWindow, set orignial to normal');
                            askTab(oldTablId, 'SET_STATUS', {
                                status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL,
                            });
                        })
                        .then(async () => {
                            await updateState((state) => ({
                                ...state,
                                tabIds: {
                                    ...state.tabIds,
                                    lastPlay: state.tabIds.toPlay,
                                    toPlay: tab.id,
                                },
                            }));

                            return activateTab(tab.id);
                        });
                })
                .catch((e: any) => {
                    if (e.message.includes('DOM failed to be ready in')) {
                        throw e;
                    }
                    //new Error(`failed to find the tab with locator '${args.target}'`)
                    /*IN case when index 0 tab not found*/
                    return Promise.all([Ext.windows.getCurrent()]).then((window: any) => {
                        return Ext.tabs.query({ active: true, windowId: window.id }).then(async (tabs: any) => {
                            // lw:排除popup
                            tabs = tabs.filter((tab: any) => tab.url !== Ext.runtime.getURL('panel.html'));
                            if (!tabs || !tabs.length) return false;
                            log('in initPlayTab, set toPlay to', tabs[0]);
                            const ctab = tabs.filter((r: any) => r.active === true && r.url.indexOf('chrome-extension://') == -1);
                            const offset = parseInt(locator, 10);
                            let wt = await checkTabIsPresent(ctab[0].index + offset, tabs[0].windowId);
                            let tab = wt == '' ? ctab[0] : wt;
                            if ((tab.index == 0 && offset == 0) || wt != '') {
                                //when playtab index is 0
                                await updateState((state) => ({
                                    ...state,
                                    tabIds: {
                                        ...state.tabIds,
                                        lastPlay: state.tabIds.toPlay,
                                        toPlay: tab.id,
                                        firstPlay: ctab[0].id,
                                    },
                                }));
                                return activateTab(tab.id);
                            } else {
                                throw new Error(`E212: failed to find the tab with locator '${args.target}'`);
                                //log.error(e.stack)
                                //throw e
                            }
                        });
                    });
                    //throw new Error(`failed to find the tab with locator '${args.target}'`)
                });
        }

        case 'CS_TIMEOUT_STATUS':
            return Promise.resolve().then(() => askPanel('TIMEOUT_STATUS', args));

        case 'CS_DELETE_ALL_COOKIES': {
            const { url } = args;

            return Ext.cookies.getAll({ url }).then((cookies: any) => {
                const ps = cookies.map((c: any) =>
                    Ext.cookies.remove({
                        url: `${url}${c.path}`,
                        name: c.name,
                    })
                );

                return Promise.all(ps);
            });
        }

        case 'CS_SET_FILE_INPUT_FILES': {
            return Ext.extension
                .isAllowedFileSchemeAccess()
                .then((isAllowed: any) => {
                    if (!isAllowed) {
                        throw new Error('E510: Please allow access to file urls');
                    }
                })
                .catch((e: any) => {
                    throw e;
                })
                .then(() => {
                    return setFileInputFiles({
                        tabId: args.sender.tab.id,
                        selector: args.selector,
                        files: args.files,
                    });
                });
        }

        case 'CS_ON_DOWNLOAD': {
            const p = getDownloadMan().prepareDownload(args.fileName, {
                wait: !!args.wait,
                timeout: args.timeout,
                timeoutForStart: args.timeoutForStart,
            });
            return true;
        }
        //  lw:content_script调用,执行用例
        case 'CS_INVOKE': {
            const tabId = args.sender.tab.id;
            await updateState(setIn(['tabIds', 'toInvoke'], tabId));

            return storage.get('config').then(async (config = {}) => {
                const state = await getState();
                const tabId = state.tabIds.toPlay;
                const wTab = tabId != '' ? await checkWindowisOpen(tabId) : '';
                const tab: any = wTab != '' ? wTab : await getToplayTabId();
                await updateState((state: any) => ({
                    ...state,
                    tabIds: {
                        ...state.tabIds,
                        lastPlay: state.tabIds.lastPlay,
                        toPlay: tab.id,
                        firstPlay: tab.id,
                    },
                }));

                const from = (args.testCase && args.testCase.from) || (args.testSuite && args.testSuite.from);

                switch (from) {
                    case 'bookmark': {
                        if (!config.allowRunFromBookmark) {
                            throw new Error('[Message from RPA] Error E103: To run a macro or a test suite from bookmarks, you need to allow it in the RPA IDE settings first');
                        }
                        break;
                    }

                    case 'html': {
                        const isFileSchema = /^file:\/\//.test(args.sender.url);
                        const isHttpSchema = /^https?:\/\//.test(args.sender.url);

                        if (isFileSchema && !config?.allowRunFromFileSchema) {
                            throw new Error('Error #103: To run test suite from local file, enable it in RPA IDE settings first');
                        }

                        if (isHttpSchema && !config?.allowRunFromHttpSchema) {
                            throw new Error('Error #104: To run test suite from public website, enable it in RPA IDE settings first');
                        }

                        break;
                    }

                    default:
                        throw new Error('E212: unknown source not allowed');
                }

                return showPanelWindow({
                    minimized: true, // lw: IDE最小化方式
                    params: { from },
                }).then(() => {

                    if (args.testCase) {
                        return askPanel('RUN_TEST_CASE', {
                            testCase: args.testCase,
                            options: args.options,
                        });
                    }

                    if (args.testSuite) {
                        return askPanel('RUN_TEST_SUITE', {
                            testSuite: args.testSuite,
                            options: args.options,
                        });
                    }

                    return true;
                });
            });
        }
        // content_script调用导入并执行
        case 'CS_IMPORT_AND_INVOKE': {
            const from = args.from;

            return storage.get('config').then((config = {}) => {
                const isFileSchema = /^file:\/\//.test(args.sender.url);
                const isHttpSchema = /^https?:\/\//.test(args.sender.url);

                if (isFileSchema && !config?.allowRunFromFileSchema) {
                    throw new Error('Error #105: To run macro from local file, enable it in RPA settings first');
                }

                if (isHttpSchema && !config?.allowRunFromHttpSchema) {
                    throw new Error('Error #105: To run macro from public website, enable it in the RPA settings first');
                }

                return showPanelWindow({
                    minimized: true, // lw: IDE最小化方式
                    params: { from },
                }).then(() => {
                    return askPanel('IMPORT_AND_RUN', args);
                });
            });
        }

        case 'CS_ADD_LOG': {
            return Promise.resolve().then(() => askPanel('ADD_LOG', args));
        }

        case 'CS_OPEN_PANEL_SETTINGS': {
            showPanelWindow({
                params: { settings: true },
            })
                .then(() => {
                    return askPanel('OPEN_SETTINGS', {});
                })
                .catch((e) => {
                    console.error(e);
                });
            return true;
        }

        case 'DESKTOP_EDITOR_ADD_VISION_IMAGE': {
            return showPanelWindow({}).then(() => {
                return askPanel('ADD_VISION_IMAGE', {
                    dataUrl: args.dataUrl,
                    requireRename: true,
                });
            });
        }

        case 'TIMEOUT': {
            // log('TIMEOUT', args.timeout, args.id)
            return delay(() => args.id, args.timeout);
        }

        default:
            return 'unknown';
    }
};

const initIPC = async () => {
    Ext.runtime.onMessage.addListener((req: any, sender: any, sendResponse: any) => {
        if (req.cmd == 'PANEL_LOG') {
            return console.log(req.args.log)
        }

        switch (req.type) {
            case 'BringIDEToFront': {
                let delay = req.delay || 0
                setTimeout(() => {
                    Ext.windows.update(req.windowId, { focused: true })
                }, delay)
                break
            }
            case 'CS_ANSWER_BG':
            case 'CS_ASK_BG': {
                onRequest(req.cmd, {
                    ...req.args || {}, sender: {
                        tab: {
                            id: sender.tab?.id
                        },
                        url: sender.url
                    }
                }).then(res => {
                    sendResponse(res)
                }).catch((e: any) => {
                    console.error('CS_ASK_BG Error ==:>> ', e)
                    sendResponse({ success: false, error: e.message })
                })
            }
            default: {

            }
        }

        return true
    })
};

const initOnInstalled = () => {
    if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') {
        Ext.runtime.setUninstallURL(config.urlAfterUninstall);

        Ext.runtime.onInstalled.addListener(({ reason, previousVersion }: any) => {
            // * Why doesn't it fire in firefox?
            switch (reason) {
                case 'install': {
                    storage.get('config').then((config: any) => {
                        return storage.set('config', {
                            ...config,
                            recordNotification: true,
                            showTestCaseTab: false,
                        });
                    });

                    return Ext.tabs.create({
                        url: config.urlAfterInstall,
                    });
                }

                case 'update': {
                    Ext.action.setBadgeText({ text: 'NEW' });
                    Ext.action.setBadgeBackgroundColor({ color: '#4444FF' });
                    return Ext.storage.local.set({
                        upgrade_not_viewed: 'not_viewed',
                    });
                }
            }
        });
    }
};

// With service worker, this method could be called multiple times as background,
// must make sure that it only set those tabIds when it's in normal mode
// (not playing/recording/inspecting)
const initPlayTab = () => {
    return Promise.all([Ext.windows.getCurrent(), getState()]).then(([window, state]) => {
        // *** this line has been fixed. Look for any unintended side effects ***
        // console.log('state:>> ', state)
        // console.log('window:>> ', window)
        if (state.status !== CONSTANT.APP_STATUS.NORMAL) {
            return false;
        }

        return Ext.tabs.query({ active: true, windowId: window.id }).then(async (tabs: any) => {
            // lw:排除panel
            tabs = tabs.filter((tab: any) => tab.url !== Ext.runtime.getURL('panel.html'));
            if (!tabs || !tabs.length) return false;
            if (tabs[0].id === state.tabIds.panel) return false;

            log('in initPlayTab, set toPlay to', tabs[0]);

            await updateState((state: any) => ({
                ...state,
                tabIds: {
                    ...state.tabIds,
                    lastPlay: state.tabIds.toPlay,
                    toPlay: tabs[0].id,
                    firstPlay: tabs[0].id,
                },
            }));

            return true;
        });
    });
};

const initDownloadMan = () => {
    getDownloadMan().onCountDown((data: any) => {
        Promise.resolve().then(() => {
            askPanel('TIMEOUT_STATUS', {
                ...data,
                type: 'download',
            });
        });
    });

    getDownloadMan().onDownloadComplete((downloadItem: any) => {
        Promise.resolve().then(() => {
            askPanel('DOWNLOAD_COMPLETE', downloadItem);
        });
    });
};

const initProxyMan = () => {
    const onProxyChange = async (newProxy: any) => {
        const img = newProxy ? config.icons.inverted : config.icons.normal;
        Ext.action.setIcon({ path: img });

        const state = await getState();

        if (state.tabIds.panel) {
            Promise.resolve()
                .then(() => askPanel('PROXY_UPDATE', { proxy: newProxy }))
                .catch((e) => log.warn(e));
        }
    };

    getProxyManager().getProxy().then(onProxyChange);
    getProxyManager().onChange(onProxyChange);
};

bindEvents();
initIPC();
initOnInstalled();
initPlayTab();
initDownloadMan();
initProxyMan();
getContextMenuService().destroyMenus();

(self as any).clip = clipboard;


// chrome.sidePanel
//     .setPanelBehavior({ openPanelOnActionClick: true })
//     .catch((error) => console.error(error));
